<!DOCTYPE html>
<html lang=en>
<head>
    <!-- so meta -->
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="HandheldFriendly" content="True">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
    <meta name="description" content="如何使用Go建开发高负载WebSocket服务器今天我们将讨论API或WebSocket服务器。我们的服务器将有大约300万个在线连接。 作者：佚名来源：推酷| 2017-09-13 14:46  收藏   分享 【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？嗨，大家好! 我的名字是Sergey Kamardin，我是Mail.Ru的工程师。  介绍 首先介绍我们的故事的上">
<meta property="og:type" content="article">
<meta property="og:title" content="如何使用Go建开发高负载WebSocket服务器">
<meta property="og:url" content="http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/index.html">
<meta property="og:site_name" content="TmoonSite">
<meta property="og:description" content="如何使用Go建开发高负载WebSocket服务器今天我们将讨论API或WebSocket服务器。我们的服务器将有大约300万个在线连接。 作者：佚名来源：推酷| 2017-09-13 14:46  收藏   分享 【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？嗨，大家好! 我的名字是Sergey Kamardin，我是Mail.Ru的工程师。  介绍 首先介绍我们的故事的上">
<meta property="og:locale" content="en_US">
<meta property="article:published_time" content="2017-05-08T07:38:59.000Z">
<meta property="article:modified_time" content="2020-01-07T10:48:14.847Z">
<meta property="article:author" content="Tmoonlight">
<meta name="twitter:card" content="summary">
    
    
        
          
              <link rel="shortcut icon" href="/images/favicon.ico">
          
        
        
          
            <link rel="icon" type="image/png" href="/images/logo2.gif" sizes="192x192">
          
        
        
          
            <link rel="apple-touch-icon" sizes="180x180" href="/images/logo2.gif">
          
        
    
    <!-- title -->
    <title>如何使用Go建开发高负载WebSocket服务器</title>
    <!-- styles -->
    
<link rel="stylesheet" href="/css/style.css">

    <!-- persian styles -->
    
      
<link rel="stylesheet" href="/css/rtl.css">

    
    <!-- rss -->
    
    
<meta name="generator" content="Hexo 4.2.0"></head>

<body class="max-width mx-auto px3 ltr">
    
      <div id="header-post">
  <a id="menu-icon" href="#"><i class="fas fa-bars fa-lg"></i></a>
  <a id="menu-icon-tablet" href="#"><i class="fas fa-bars fa-lg"></i></a>
  <a id="top-icon-tablet" href="#" onclick="$('html, body').animate({ scrollTop: 0 }, 'fast');" style="display:none;"><i class="fas fa-chevron-up fa-lg"></i></a>
  <span id="menu">
    <span id="nav">
      <ul>
         
          <li><a href="/">Home</a></li>
         
          <li><a href="/about/">About</a></li>
         
          <li><a href="/archives/">Writing</a></li>
         
          <li><a href="/projects_url">Projects</a></li>
        
      </ul>
    </span>
    <br/>
    <span id="actions">
      <ul>
        
        <li><a class="icon" href="/2017/05/20/gowebsocket/"><i class="fas fa-chevron-left" aria-hidden="true" onmouseover="$('#i-prev').toggle();" onmouseout="$('#i-prev').toggle();"></i></a></li>
        
        
        <li><a class="icon" href="/2017/04/26/%E8%B0%83%E6%98%AF%E9%9F%B3%E4%B9%90%E7%9A%84%E5%9B%BA%E5%AE%9A%E9%AB%98%E5%BA%A6%EF%BC%8C%E8%B0%83%E5%8F%B7%E6%98%AF%E8%B0%83%E7%9A%84%E8%AE%B0%E5%8F%B7%EF%BC%8C%E5%86%99%E5%9C%A8%E6%AF%8F%E8%A1%8C%E4%BA%94%E7%BA%BF%E8%B0%B1%E7%9A%84%E5%BC%80%E7%AB%AF%E8%B0%B1%E5%8F%B7%E5%90%8E%E9%9D%A2%E7%9A%84%E5%8D%87%E3%80%81%E9%99%8D%E8%AE%B0%E5%8F%B7%E5%B0%B1%E6%98%AF%E8%B0%83%E5%8F%B7%E3%80%82/"><i class="fas fa-chevron-right" aria-hidden="true" onmouseover="$('#i-next').toggle();" onmouseout="$('#i-next').toggle();"></i></a></li>
        
        <li><a class="icon" href="#" onclick="$('html, body').animate({ scrollTop: 0 }, 'fast');"><i class="fas fa-chevron-up" aria-hidden="true" onmouseover="$('#i-top').toggle();" onmouseout="$('#i-top').toggle();"></i></a></li>
        <li><a class="icon" href="#"><i class="fas fa-share-alt" aria-hidden="true" onmouseover="$('#i-share').toggle();" onmouseout="$('#i-share').toggle();" onclick="$('#share').toggle();return false;"></i></a></li>
      </ul>
      <span id="i-prev" class="info" style="display:none;">Previous post</span>
      <span id="i-next" class="info" style="display:none;">Next post</span>
      <span id="i-top" class="info" style="display:none;">Back to top</span>
      <span id="i-share" class="info" style="display:none;">Share post</span>
    </span>
    <br/>
    <div id="share" style="display: none">
      <ul>
  <li><a class="icon" href="http://www.facebook.com/sharer.php?u=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/" target="_blank" rel="noopener"><i class="fab fa-facebook " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://twitter.com/share?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&text=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-twitter " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.linkedin.com/shareArticle?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-linkedin " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://pinterest.com/pin/create/bookmarklet/?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&is_video=false&description=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-pinterest " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="mailto:?subject=如何使用Go建开发高负载WebSocket服务器&body=Check out this article: http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/"><i class="fas fa-envelope " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://getpocket.com/save?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-get-pocket " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://reddit.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-reddit " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.stumbleupon.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-stumbleupon " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://digg.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-digg " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.tumblr.com/share/link?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&name=如何使用Go建开发高负载WebSocket服务器&description=" target="_blank" rel="noopener"><i class="fab fa-tumblr " aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://news.ycombinator.com/submitlink?u=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&t=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-hacker-news " aria-hidden="true"></i></a></li>
</ul>

    </div>
    <div id="toc">
      <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#如何使用Go建开发高负载WebSocket服务器"><span class="toc-number">1.</span> <span class="toc-text">如何使用Go建开发高负载WebSocket服务器</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？"><span class="toc-number">2.</span> <span class="toc-text">【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？</span></a></li></ol>
    </div>
  </span>
</div>

    
    <div class="content index py4">
        
        <article class="post" itemscope itemtype="http://schema.org/BlogPosting">
  <header>
    
    <h1 class="posttitle" itemprop="name headline">
        如何使用Go建开发高负载WebSocket服务器
    </h1>



    <div class="meta">
      <span class="author" itemprop="author" itemscope itemtype="http://schema.org/Person">
        <span itemprop="name">TmoonSite</span>
      </span>
      
    <div class="postdate">
      
        <time datetime="2017-05-08T07:38:59.000Z" itemprop="datePublished">2017-05-08</time>
        
      
    </div>


      

      

    </div>
  </header>
  

  <div class="content" itemprop="articleBody">
    <h2 id="如何使用Go建开发高负载WebSocket服务器"><a href="#如何使用Go建开发高负载WebSocket服务器" class="headerlink" title="如何使用Go建开发高负载WebSocket服务器"></a>如何使用Go建开发高负载WebSocket服务器</h2><p>今天我们将讨论API或WebSocket服务器。我们的服务器将有大约300万个在线连接。</p>
<p>作者：佚名来源：推酷| <em>2017-09-13 14:46</em></p>
<p> 收藏</p>
<p>  分享</p>
<h2 id="【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？"><a href="#【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？" class="headerlink" title="【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？"></a><a href="http://www.51cto.com/act/cloud/home" target="_blank" rel="noopener">【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？</a></h2><p>嗨，大家好! 我的名字是Sergey Kamardin，我是Mail.Ru的工程师。</p>
<p> <strong>介绍</strong></p>
<p>首先介绍我们的故事的上下文，应该介绍几点我们为什么需要这个服务器。</p>
<p>Mail.Ru有很多有状态的系统。 用户电子邮件存储是其中之一。 跟踪系统中的状态变化和系统事件有几种方法。 这主要是通过定期系统轮询或关于其状态变化的系统通知。</p>
<p><a href="http://s2.51cto.com/wyfs02/M00/A5/2A/wKioL1m42ODge2xyAACEhKVlgqI054.jpg-wh_651x-s_4239106702.jpg" target="_blank" rel="noopener"></a></p>
<p>两种方式都有利弊。 但是当涉及邮件时，用户收到新邮件的速度越快越好。</p>
<p>邮件轮询涉及每秒大约50,000个HTTP查询，其中60%返回304状态，这意味着邮箱没有变化。</p>
<p>因此，为了减少服务器上的负载并加快邮件传递给用户，决定通过编写发布-订阅服务器，一方面将接收有关状态更改的通知，另一方面则会收到这种通知的订阅。</p>
<p> <strong>先前</strong></p>
<p><a href="http://s1.51cto.com/wyfs02/M02/06/7A/wKiom1m41zSz9eXKAAAoxiNPsO8881.png" target="_blank" rel="noopener"></a></p>
<p> <strong>现在</strong></p>
<p><a href="http://s3.51cto.com/wyfs02/M00/06/7A/wKiom1m410bwT-9QAAAcU0pX9rE32.jpeg" target="_blank" rel="noopener"></a></p>
<p>第一个方案显示了以前的样子。 浏览器定期轮询API，并查询有关Storage(邮箱服务)的更改。</p>
<p>第二个方案描述了新架构。 浏览器与通知API建立WebSocket连接，通知API是Bus服务器的客户端。收到新的电子邮件后，Storage会向Bus(1)发送一条通知，由Bus发送到订阅者。 API确定连接以发送接收到的通知，并将其发送到用户的浏览器(3)。</p>
<p>所以今天我们将讨论API或WebSocket服务器。 我们的服务器将有大约300万个在线连接。</p>
<p> <strong>实现方式</strong></p>
<p>让我们看看如何使用Go函数实现服务器的某些部分，而无需任何优化。</p>
<p>在进行net/http ，我们来谈谈我们如何发送和接收数据。 站在WebSocket协议(例如JSON对象) 之上的数据在下文中将被称为分组 。</p>
<p>我们开始实现包含通过WebSocket连接发送和接收这些数据包的Channel结构。</p>
<p> <strong>channel 结构</strong></p>
<ol>
<li>// Packet represents application level data. </li>
<li>type Packet struct { </li>
<li>… </li>
<li>} </li>
<li></li>
<li>// Channel wraps user connection. </li>
<li>type Channel struct { </li>
<li>conn net.Conn    // WebSocket connection. </li>
<li>send chan Packet // Outgoing packets queue. </li>
<li>} </li>
<li></li>
<li>func NewChannel(conn net.Conn) *Channel { </li>
<li>c := &amp;Channel{ </li>
<li>conn: conn, </li>
<li>send: make(chan Packet, N), </li>
<li>} </li>
<li></li>
<li>go c.reader() </li>
<li>go c.writer() </li>
<li></li>
<li>return c </li>
<li>} </li>
</ol>
<p>注意这里有reader和writer连个goroutines。 每个goroutine都需要自己的内存栈， 根据操作系统和Go版本可能具有2到8 KB的初始大小。</p>
<p>在300万个在线连接的时候，我们将需要24 GB的内存 (堆栈为4 KB)用于维持所有连接。 这还没有计算为Channel结构分配的内存，传出的数据包ch.send和其他内部字段消耗的内存。</p>
<ol>
<li>I/O goroutines </li>
</ol>
<p>我们来看看“reader”的实现：</p>
<ol>
<li>func (c *Channel) reader() { </li>
<li>// We make a buffered read to reduce read syscalls. </li>
<li>buf := bufio.NewReader(c.conn) </li>
<li></li>
<li>for { </li>
<li>pkt, _ := readPacket(buf) </li>
<li>c.handle(pkt) </li>
<li>} </li>
<li>} </li>
</ol>
<p>这里我们使用bufio.Reader来减少read() syscalls的数量，并读取与buf缓冲区大小一样的数量。 在无限循环中，我们期待新数据的到来。 请记住： 预计新数据将会来临。 我们稍后会回来。</p>
<p>我们将离开传入数据包的解析和处理，因为对我们将要讨论的优化不重要。 但是， buf现在值得我们注意：默认情况下，它是4 KB，这意味着我们需要另外12 GB内存。 “writer”有类似的情况：</p>
<ol>
<li>func (c *Channel) writer() { </li>
<li>// We make buffered write to reduce write syscalls.  </li>
<li>buf := bufio.NewWriter(c.conn) </li>
<li></li>
<li>for pkt := range c.send { </li>
<li>_ := writePacket(buf, pkt) </li>
<li>buf.Flush() </li>
<li>} </li>
<li>} </li>
</ol>
<p>我们遍历c.send ，并将它们写入缓冲区。细心读者已经猜到的，我们的300万个连接还将消耗12 GB的内存。</p>
<p> <strong>HTTP</strong></p>
<p>我们已经有一个简单的Channel实现，现在我们需要一个WebSocket连接才能使用。</p>
<p>注意：如果您不知道WebSocket如何工作。客户端通过称为升级的特殊HTTP机制切换到WebSocket协议。 在成功处理升级请求后，服务器和客户端使用TCP连接来交换二进制WebSocket帧。 这是连接中的框架结构的描述。</p>
<ol>
<li>import ( </li>
<li>“net/http” </li>
<li>“some/websocket” </li>
<li>) </li>
<li></li>
<li>http.HandleFunc(“/v1/ws”, func(w http.ResponseWriter, r *http.Request) { </li>
<li>conn, _ := websocket.Upgrade(r, w) </li>
<li>ch := NewChannel(conn) </li>
<li>//… </li>
<li>}) </li>
</ol>
<p>请注意， http.ResponseWriter为bufio.Reader和bufio.Writer (使用4 KB缓冲区)进行内存分配，用于*http.Request初始化和进一步的响应写入。</p>
<p>无论使用什么WebSocket库，在成功响应升级请求后， 服务器在responseWriter.Hijack()调用之后，连同TCP连接一起接收 I/O缓冲区。</p>
<p>提示：在某些情况下， go:linkname 可用于 通过调用 net/http.putBufio{Reader,Writer} 将缓冲区返回到 net/http 内 的 sync.Pool 。</p>
<p>因此，我们需要另外24 GB的内存来维持300万个链接。</p>
<p>所以，我们的程序即使什么都没做，也需要72G内存。</p>
<p> <strong>优化</strong></p>
<p>我们来回顾介绍部分中谈到的内容，并记住用户连接的行为。 切换到WebSocket之后，客户端发送一个包含相关事件的数据包，换句话说就是订阅事件。 然后(不考虑诸如ping/pong等技术信息)，客户端可能在整个连接寿命中不发送任何其他信息。</p>
<p>连接寿命可能是几秒到几天。</p>
<p>所以在最多的时候，我们的Channel.reader()和Channel.writer()正在等待接收或发送数据的处理。 每个都有4 KB的I/O缓冲区。</p>
<p>现在很明显，某些事情可以做得更好，不是吗?</p>
<p> <strong>Netpoll</strong></p>
<p>你还记得bufio.Reader.Read()内部，Channel.reader()实现了在没有新数据的时候conn.read()会被锁。如果连接中有数据，Go运行时“唤醒”我们的goroutine并允许它读取下一个数据包。 之后，goroutine再次锁定，期待新的数据。 让我们看看Go运行时如何理解goroutine必须被“唤醒”。 如果我们看看conn.Read()实现 ，我们将在其中看到net.netFD.Read()调用 ：</p>
<ol>
<li>// net/fd_unix.go </li>
<li></li>
<li>func (fd *netFD) Read(p []byte) (n int, err error) { </li>
<li>//… </li>
<li>for { </li>
<li>n, err = syscall.Read(fd.sysfd, p) </li>
<li>if err != nil { </li>
<li>n = 0 </li>
<li>if err == syscall.EAGAIN { </li>
<li>if err = fd.pd.waitRead(); err == nil { </li>
<li>continue </li>
<li>} </li>
<li>} </li>
<li>} </li>
<li>//… </li>
<li>break </li>
<li>} </li>
<li>//… </li>
<li>} </li>
</ol>
<p>Go在非阻塞模式下使用套接字。 EAGAIN表示，套接字中没有数据，并且在从空套接字读取时不会被锁定，操作系统将控制权返还给我们。</p>
<p>我们从连接文件描述符中看到一个read()系统调用。 如果读取返回EAGAIN错误 ，则运行时会使pollDesc.waitRead()调用 ：</p>
<ol>
<li>// net/fd_poll_runtime.go </li>
<li></li>
<li>func (pd *pollDesc) waitRead() error { </li>
<li>return pd.wait(‘r’) </li>
<li>} </li>
<li></li>
<li>func (pd *pollDesc) wait(mode int) error { </li>
<li>res := runtime_pollWait(pd.runtimeCtx, mode) </li>
<li>//… </li>
<li>} </li>
</ol>
<p>如果我们深入挖掘 ，我们将看到netpoll是使用Linux中的epoll和BSD中的kqueue来实现的。 为什么不使用相同的方法来进行连接? 我们可以分配一个读缓冲区，只有在真正有必要时才使用goroutine：当套接字中有真实可读的数据时。</p>
<p>在github.com/golang/go上， 导出netpoll函数有问题 。</p>
<p> <strong>摆脱goroutines</strong></p>
<p>假设我们有Go的netpoll实现 。 现在我们可以避免使用内部缓冲区启动Channel.reader() goroutine，并在连接中订阅可读数据的事件：</p>
<ol>
<li>ch := NewChannel(conn) </li>
<li></li>
<li>// Make conn to be observed by netpoll instance. </li>
<li>poller.Start(conn, netpoll.EventRead, func() { </li>
<li>// We spawn goroutine here to prevent poller wait loop </li>
<li>// to become locked during receiving packet from ch. </li>
<li>go Receive(ch) </li>
<li>}) </li>
<li></li>
<li>// Receive reads a packet from conn and handles it somehow. </li>
<li>func (ch *Channel) Receive() { </li>
<li>buf := bufio.NewReader(ch.conn) </li>
<li>pkt := readPacket(buf) </li>
<li>c.handle(pkt) </li>
<li>} </li>
</ol>
<p>使用Channel.writer()更容易，因为只有当我们要发送数据包时，我们才能运行goroutine并分配缓冲区：</p>
<ol>
<li>func (ch *Channel) Send(p Packet) { </li>
<li>if c.noWriterYet() { </li>
<li>go ch.writer() </li>
<li>} </li>
<li>ch.send &lt;- p </li>
<li>} </li>
</ol>
<p>请注意，当操作系统在 write() 系统调用时返回 EAGAIN 时，我们不处理这种情况 。 对于这种情况，我们倾向于Go运行时那样处理。 如果需要，它可以以相同的方式来处理。</p>
<p>从ch.send (一个或几个)读出传出的数据包后，writer将完成其操作并释放goroutine栈和发送缓冲区。</p>
<p>完美! 通过摆脱两个连续运行的goroutine中的堆栈和I/O缓冲区，我们节省了48 GB 。</p>
<p> <strong>资源控制</strong></p>
<p>大量的连接不仅涉及高内存消耗。 在开发服务器时，我们会经历重复的竞争条件和死锁，常常是所谓的自动DDoS，这种情况是当应用程序客户端肆意尝试连接到服务器，从而破坏服务器。</p>
<p>例如，如果由于某些原因我们突然无法处理ping/pong消息，但是空闲连接的处理程序会关闭这样的连接(假设连接断开，因此没有提供数据)，客户端会不断尝试连接，而不是等待事件。</p>
<p>如果锁定或超载的服务器刚刚停止接受新连接，并且负载均衡器(例如，nginx)将请求都传递给下一个服务器实例，那压力将是巨大的。</p>
<p>此外，无论服务器负载如何，如果所有客户端突然想要以任何原因发送数据包(大概是由于错误原因)，则先前节省的48 GB将再次使用，因为我们将实际恢复到初始状态goroutine和并对每个连接分配缓冲区。</p>
<p> <strong>Goroutine池</strong></p>
<p>我们可以使用goroutine池来限制同时处理的数据包数量。 这是一个go routine池的简单实现：</p>
<ol>
<li>package gopool </li>
<li></li>
<li>func New(size int) *Pool { </li>
<li>return &amp;Pool{ </li>
<li>work: make(chan func()), </li>
<li>sem:  make(chan struct{}, size), </li>
<li>} </li>
<li>} </li>
<li></li>
<li>func (p *Pool) Schedule(task func()) error { </li>
<li>select { </li>
<li>case p.work &lt;- task: </li>
<li>case p.sem &lt;- struct{}{}: </li>
<li>go p.worker(task) </li>
<li>} </li>
<li>} </li>
<li></li>
<li>func (p *Pool) worker(task func()) { </li>
<li>defer func() { &lt;-p.sem } </li>
<li>for { </li>
<li>task() </li>
<li>task = &lt;-p.work </li>
<li>} </li>
<li>} </li>
</ol>
<p>现在我们的netpoll代码如下：</p>
<ol>
<li>pool := gopool.New(128) </li>
<li></li>
<li>poller.Start(conn, netpoll.EventRead, func() { </li>
<li>// We will block poller wait loop when </li>
<li>// all pool workers are busy. </li>
<li>pool.Schedule(func() { </li>
<li>Receive(ch) </li>
<li>}) </li>
<li>}) </li>
</ol>
<p>所以现在我们读取数据包可以在池中使用了空闲的goroutine。</p>
<p>同样，我们将更改Send() ：</p>
<ol>
<li>pool := gopool.New(128) </li>
<li></li>
<li>func (ch *Channel) Send(p Packet) { </li>
<li>if c.noWriterYet() { </li>
<li>pool.Schedule(ch.writer) </li>
<li>} </li>
<li>ch.send &lt;- p </li>
<li>} </li>
</ol>
<p>而不是go ch.writer() ，我们想写一个重用的goroutine。 因此，对于N goroutines池，我们可以保证在N请求同时处理并且到达N + 1我们不会分配N + 1缓冲区进行读取。 goroutine池还允许我们限制新连接的Accept()和Upgrade() ，并避免大多数情况下被DDoS打垮。</p>
<p> <strong>零拷贝升级</strong></p>
<p>让我们从WebSocket协议中偏离一点。 如前所述，客户端使用HTTP升级请求切换到WebSocket协议。 协议是样子：</p>
<ol>
<li>GET /ws HTTP/1.1 </li>
<li>Host: mail.ru </li>
<li>Connection: Upgrade </li>
<li>Sec-Websocket-Key: A3xNe7sEB9HixkmBhVrYaA== </li>
<li>Sec-Websocket-Version: 13 </li>
<li>Upgrade: websocket </li>
<li></li>
<li>HTTP/1.1 101 Switching Protocols </li>
<li>Connection: Upgrade </li>
<li>Sec-Websocket-Accept: ksu0wXWG+YmkVx+KQR2agP0cQn4= </li>
<li>Upgrade: websocket </li>
</ol>
<p>也就是说，在我们的例子中，我们需要HTTP请求和header才能切换到WebSocket协议。 这个知识点和http.Request的内部实现表明我们可以做优化。我们会在处理HTTP请求时抛弃不必要的内存分配和复制，并放弃标准的net/http服务器。</p>
<p>例如， http.Request 包含一个具有相同名称的头文件类型的字段，它通过将数据从连接复制到值字符串而无条件填充所有请求头。 想像一下这个字段中可以保留多少额外的数据，例如大型Cookie头。</p>
<p>但是要做什么呢?</p>
<p> <strong>WebSocket实现</strong></p>
<p>不幸的是，在我们的服务器优化时存在的所有库都允许我们对标准的net/http服务器进行升级。 此外，所有库都不能使用所有上述读写优化。 为使这些优化能够正常工作，我们必须使用一个相当低级别的API来处理WebSocket。 要重用缓冲区，我们需要procotol函数看起来像这样：</p>
<ol>
<li>func ReadFrame(io.Reader) (Frame, error)  </li>
<li>func WriteFrame(io.Writer, Frame) error </li>
</ol>
<p>如果我们有一个这样的API的库，我们可以从连接中读取数据包，如下所示(数据包写入看起来差不多)：</p>
<ol>
<li>// getReadBuf, putReadBuf are intended to  </li>
<li>// reuse *bufio.Reader (with sync.Pool for example). </li>
<li>func getReadBuf(io.Reader) *bufio.Reader </li>
<li>func putReadBuf(*bufio.Reader) </li>
<li></li>
<li>// readPacket must be called when data could be read from conn. </li>
<li>func readPacket(conn io.Reader) error { </li>
<li>buf := getReadBuf() </li>
<li>defer putReadBuf(buf) </li>
<li></li>
<li>buf.Reset(conn) </li>
<li>frame, _ := ReadFrame(buf) </li>
<li>parsePacket(frame.Payload) </li>
<li>//… </li>
<li>} </li>
</ol>
<p>简而言之，现在是制作我们自己库的时候了。</p>
<ol>
<li>github.com/gobwas/ws </li>
</ol>
<p>为了避免将协议操作逻辑强加给用户，我们编写了WS库。 所有读写方法都接受标准的io.Reader和io.Writer接口，可以使用或不使用缓冲或任何其他I/O包装器。</p>
<p>除了来自标准net/http升级请求之外， ws支持零拷贝升级 ，升级请求的处理和切换到WebSocket，而无需内存分配或复制。 ws.Upgrade()接受io.ReadWriter ( net.Conn实现了这个接口)。 换句话说，我们可以使用标准的net.Listen()并将接收到的连接从ln.Accept()立即传递给ws.Upgrade() 。 该库可以复制任何请求数据以供将来在应用程序中使用(例如， Cookie以验证会话)。</p>
<p>以下是升级请求处理的基准 ：标准net/http服务器与net.Listen()加零拷贝升级：</p>
<ol>
<li>BenchmarkUpgradeHTTP 5156 ns/op 8576 B/op 9 allocs/op  </li>
<li>BenchmarkUpgradeTCP 973 ns/op 0 B/op 0 allocs/op </li>
</ol>
<p>切换到ws和零拷贝升级节省了另外24 GB内存 - 这是由net/http处理程序请求处理时为I/O缓冲区分配的空间。</p>
<p> <strong>概要</strong></p>
<p>让我们结合代码告诉你我们做的优化。</p>
<ul>
<li>读取内部缓冲区的goroutine是非常昂贵的。 解决方案 ：netpoll(epoll，kqueue); 重用缓冲区。</li>
<li>写入内部缓冲区的goroutine是非常昂贵的。 解决方案 ：必要时启动goroutine; 重用缓冲区。</li>
<li>DDOS，netpoll将无法工作。 解决方案 ：重新使用数量限制的goroutines。</li>
<li>net/http不是处理升级到WebSocket的最快方法。 解决方案 ：在连接上使用零拷贝升级。</li>
</ul>
<p>这就是服务器代码的样子：</p>
<ol>
<li>import ( </li>
<li>“net” </li>
<li>“github.com/gobwas/ws” </li>
<li>) </li>
<li></li>
<li>ln, _ := net.Listen(“tcp”, “:8080”) </li>
<li></li>
<li>for { </li>
<li>// Try to accept incoming connection inside free pool worker. </li>
<li>// If there no free workers for 1ms, do not accept anything and try later. </li>
<li>// This will help us to prevent many self-ddos or out of resource limit cases. </li>
<li>err := pool.ScheduleTimeout(time.Millisecond, func() { </li>
<li>conn := ln.Accept() </li>
<li>_ = ws.Upgrade(conn) </li>
<li></li>
<li>// Wrap WebSocket connection with our Channel struct. </li>
<li>// This will help us to handle/send our app’s packets. </li>
<li>ch := NewChannel(conn) </li>
<li></li>
<li>// Wait for incoming bytes from connection. </li>
<li>poller.Start(conn, netpoll.EventRead, func() { </li>
<li>// Do not cross the resource limits. </li>
<li>pool.Schedule(func() { </li>
<li>// Read and handle incoming packet(s). </li>
<li>ch.Recevie() </li>
<li>}) </li>
<li>}) </li>
<li>}) </li>
<li>if err != nil {    </li>
<li>time.Sleep(time.Millisecond) </li>
<li>} </li>
<li>} </li>
</ol>
<p> <strong>结论</strong></p>
<p>过早优化是万恶之源。 Donald Knuth</p>
<p>当然，上述优化是有意义的，但并非所有情况都如此。 例如，如果可用资源(内存，CPU)和在线连接数之间的比例相当高(服务器很闲)，则优化可能没有任何意义。 但是，您可以从哪里需要改进以及改进内容中受益匪浅。</p>

  </div>
</article>



        
          <div id="footer-post-container">
  <div id="footer-post">

    <div id="nav-footer" style="display: none">
      <ul>
         
          <li><a href="/">Home</a></li>
         
          <li><a href="/about/">About</a></li>
         
          <li><a href="/archives/">Writing</a></li>
         
          <li><a href="/projects_url">Projects</a></li>
        
      </ul>
    </div>

    <div id="toc-footer" style="display: none">
      <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#如何使用Go建开发高负载WebSocket服务器"><span class="toc-number">1.</span> <span class="toc-text">如何使用Go建开发高负载WebSocket服务器</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？"><span class="toc-number">2.</span> <span class="toc-text">【限时免费】年底最强一次云计算大会，看传统、社区、互联网企业如何碰撞？</span></a></li></ol>
    </div>

    <div id="share-footer" style="display: none">
      <ul>
  <li><a class="icon" href="http://www.facebook.com/sharer.php?u=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/" target="_blank" rel="noopener"><i class="fab fa-facebook fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://twitter.com/share?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&text=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-twitter fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.linkedin.com/shareArticle?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-linkedin fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://pinterest.com/pin/create/bookmarklet/?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&is_video=false&description=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-pinterest fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="mailto:?subject=如何使用Go建开发高负载WebSocket服务器&body=Check out this article: http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/"><i class="fas fa-envelope fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://getpocket.com/save?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-get-pocket fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://reddit.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-reddit fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.stumbleupon.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-stumbleupon fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://digg.com/submit?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&title=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-digg fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="http://www.tumblr.com/share/link?url=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&name=如何使用Go建开发高负载WebSocket服务器&description=" target="_blank" rel="noopener"><i class="fab fa-tumblr fa-lg" aria-hidden="true"></i></a></li>
  <li><a class="icon" href="https://news.ycombinator.com/submitlink?u=http://yoursite.com/2017/05/08/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8Go%E5%BB%BA%E5%BC%80%E5%8F%91%E9%AB%98%E8%B4%9F%E8%BD%BDWebSocket%E6%9C%8D%E5%8A%A1%E5%99%A8/&t=如何使用Go建开发高负载WebSocket服务器" target="_blank" rel="noopener"><i class="fab fa-hacker-news fa-lg" aria-hidden="true"></i></a></li>
</ul>

    </div>

    <div id="actions-footer">
        <a id="menu" class="icon" href="#" onclick="$('#nav-footer').toggle();return false;"><i class="fas fa-bars fa-lg" aria-hidden="true"></i> Menu</a>
        <a id="toc" class="icon" href="#" onclick="$('#toc-footer').toggle();return false;"><i class="fas fa-list fa-lg" aria-hidden="true"></i> TOC</a>
        <a id="share" class="icon" href="#" onclick="$('#share-footer').toggle();return false;"><i class="fas fa-share-alt fa-lg" aria-hidden="true"></i> Share</a>
        <a id="top" style="display:none" class="icon" href="#" onclick="$('html, body').animate({ scrollTop: 0 }, 'fast');"><i class="fas fa-chevron-up fa-lg" aria-hidden="true"></i> Top</a>
    </div>

  </div>
</div>

        
        <footer id="footer">
  <div class="footer-left">
    Copyright &copy; 2020 Tmoonlight
  </div>
  <div class="footer-right">
    <nav>
      <ul>
         
          <li><a href="/">Home</a></li>
         
          <li><a href="/about/">About</a></li>
         
          <li><a href="/archives/">Writing</a></li>
         
          <li><a href="/projects_url">Projects</a></li>
        
      </ul>
    </nav>
  </div>
</footer>

    </div>
    <!-- styles -->

<link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">


<link rel="stylesheet" href="/lib/justified-gallery/css/justifiedGallery.min.css">


    <!-- jquery -->

<script src="/lib/jquery/jquery.min.js"></script>


<script src="/lib/justified-gallery/js/jquery.justifiedGallery.min.js"></script>

<!-- clipboard -->

  
<script src="/lib/clipboard/clipboard.min.js"></script>

  <script type="text/javascript">
  $(function() {
    // copy-btn HTML
    var btn = "<span class=\"btn-copy tooltipped tooltipped-sw\" aria-label=\"Copy to clipboard!\">";
    btn += '<i class="far fa-clone"></i>';
    btn += '</span>'; 
    // mount it!
    $(".highlight table").before(btn);
    var clip = new ClipboardJS('.btn-copy', {
      text: function(trigger) {
        return Array.from(trigger.nextElementSibling.querySelectorAll('.code')).reduce((str,it)=>str+it.innerText+'\n','')
      }
    });
    clip.on('success', function(e) {
      e.trigger.setAttribute('aria-label', "Copied!");
      e.clearSelection();
    })
  })
  </script>


<script src="/js/main.js"></script>

<!-- search -->

<!-- Google Analytics -->

<!-- Baidu Analytics -->

<!-- Disqus Comments -->


</body>
</html>
